/*************************************************
* Title：TinyToolKit
* Author：Opportunity 
* Describe：资源管理器
 * 资源同步加载、资源异步加载、资源卸载
 * 资源的加载分为 编辑器模式 和 AB包加载模式 
 * 编辑器模式下利用 UnityEditor.AssetDatabase 中提供的方法直接加载本地资源，而不用预先打包处理，该模式在发布后将会失效
 * AB包加载模式需要预先构建并准备完成AssetBundle，编辑器模式可以预演发布状态下的各种情况，如资源异步加载等，方便调试发布时的相关问题
* CreateTime：2020-04-04 17:08:43
* Version：1.0
* Modify Recorder：
 * 修改时间：2020-10-14
 * 加载AB资源分为：本地加载（同步加载/异步加载）、服务器加载（非缓存下载，缓存加载）
 * 加载Asset资源分为：同步加载、异步加载
*************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Object = UnityEngine.Object;

namespace TinyToolKit.AssetLoad
{
    public class TAssetBundleManager : ManagerBase
    {
        private bool _isEditorMode; //编辑器模拟模式

        //存储正在使用的资源列表,key是Path的Crc
        private Dictionary<uint, ResourceItem> assetDic;
        private ABLoadHelper _abLoadHelper;
        private ABVersionHelper _abVersionHelper;
        
        public Dictionary<uint, ResourceItem> AssetDic => assetDic;
        public Dictionary<uint, AssetBundleItem> AbItemDic => _abLoadHelper._abItemDic;

        //资源池，暂存未被使用的资源，防止常用资源频繁读取和释放，但存储达到上限，便释放最早的无用资源
        private CMapList<ResourceItem> _noRefAssetMapList; //这里的ResourceItem和ABLoadHelper中的_ResourceItemDic的ResourceItem是同一引用
        private List<LoadAssetAsyncPara>[] _loadingAssetArray; //需要异步加载的资源列表,数组的下标表示优先级，0-High,1-Middle,2-low
        private Dictionary<uint, LoadAssetAsyncPara> _loadingAssetDic; //正在异步加载的资源列表，用于过滤当前正在加载的资源，避免重复加载错误
        private const long LOADINGTIME = 200000; //连续异步加载资源的时间(微秒),1秒=1,000,000 微秒(μs),tick的单位为100纳秒，也就是0.1微妙

        #region Manager Base

        public override void Init()
        {
            assetDic = new Dictionary<uint, ResourceItem>();
            _noRefAssetMapList = new CMapList<ResourceItem>();
            _abLoadHelper = new ABLoadHelper();
            _abVersionHelper = new ABVersionHelper();

#if UNITY_EDITOR
            _isEditorMode = TinyTool.Instance.TinyToolSetting.isEditorMode;
#endif
        }

        public override void UpdateManager()
        {
        }

        public override void Shutdown()
        {
            Clear();
        }

        #endregion

        #region 加载AB资源-服务器异步下载

        /// <summary>
        /// 校准服务器和本地的AB包版本，并更新AB包资源
        /// </summary>
        /// <param name="localVersionPath">本地版本配置表的路径</param>
        /// <param name="serverVersionUrl">服务器端版本配置表的Url</param>
        /// <param name="abConfigUrl">服务器端AB包配置表的Url</param>
        /// <param name="configDataFormat">配置表的数据格式类型</param>
        /// <param name="abUriPrefix">服务器端AB包的统一前缀路径的Url</param>
        /// <param name="abConfigSavePath">AB包配置表本地保存路径</param>
        /// <param name="abSaveDirPath">AB包本地保存的文件夹路径</param>
        /// <param name="callback">AB包版本更新结束时的回调函数</param>
        public void CheckABVersion(string localVersionPath, string serverVersionUrl, string abConfigUrl, DataFormat configDataFormat, string abUriPrefix, string abConfigSavePath, string abSaveDirPath,
            Action callback = null)
        {
            //更新版本
            UpdateABVersion(localVersionPath, serverVersionUrl, configDataFormat, abSaveDirPath, abNameList =>
            {
                //下载配置表，并保存至本地
                LoadABConfigAsync(abConfigUrl, configDataFormat, abConfigSavePath, config =>
                {
                    if (abNameList == null)
                    {
                        callback?.Invoke();
                        return;
                    }

                    //下载最新的AB包
                    if (abNameList.Count > 0)
                    {
                        int count = abNameList.Count;
                        int curCount = 0;
                        DownLoadABAsync(abNameList.ToArray(), abUriPrefix, abSaveDirPath,
                            (abName, isSuccess) =>
                            {
                                if (isSuccess) curCount++;
                                if (curCount == count)
                                    callback?.Invoke();
                            });
                    }
                    else
                    {
                        TLogTool.Info("【AB包版本更新】本地AB包资源是最新版本");
                        callback?.Invoke();
                    }
                });
            });
        }

        public void UpdateABVersion(string localVersionPath, string serverVersionUrl, DataFormat configDataFormat, string abUriPrefix, Action<List<string>> callBack)
        {
            if (_isEditorMode)
            {
                callBack?.Invoke(null);
                return;
            }

            _abVersionHelper.UpdateABVersion(localVersionPath, serverVersionUrl, configDataFormat, abUriPrefix, callBack);
        }

        /// <summary>
        /// 异步加载AB包配置表
        /// </summary>
        /// <param name="url">配置表的服务器地址</param>
        /// <param name="saveDirPath">配置表保存的本地文件夹路径，如果为空，则不保存</param>
        /// <param name="callback">配置表下载完成时的回调函数</param>
        public void LoadABConfigAsync(string url, DataFormat dataFormat, string saveDirPath = "", Action<AssetBundleConfig> callback = null)
        {
            _abLoadHelper.LoadABConfigAsync(url, dataFormat, saveDirPath, callback);
        }

        /// <summary>
        /// 异步下载AB包资源，可以将AB包下载至本地，或者加载到内存中
        /// </summary>
        /// <param name="assetNames">所有需要预加载资源的名称列表</param>
        /// <param name="uriPrefix">AB包资源的统一前缀下载地址</param>
        /// <param name="saveDirPath">AB包资源保存路径，如果路径为空，则加载保存在缓存里</param>
        /// <param name="callback">资源下载完成时的回调函数</param>  
        public void LoadABResAsync(string[] assetNames, string uriPrefix, string saveDirPath = "", Action callback = null)
        {
            //如果是编辑器资源加载模式下，则跳过异步下载AB包资源的过程
            if (_isEditorMode)
            {
                callback?.Invoke();
                return;
            }

            List<string> fullPaths = GetAssetPath(assetNames);
            if (fullPaths.Count > 0)
                _abLoadHelper.LoadABAsync(fullPaths, uriPrefix, saveDirPath, callback);
            else
                TLogTool.Error("所有需要加载的资源路径为空");
        }

        /// <summary>
        /// 异步加载abNames数组中所有的AB包资源到缓存中
        /// </summary>
        /// <param name="uriPrefix">AB包资源的统一下载地址</param>
        /// <param name="abNames">要下载的AB名数组</param>
        /// <param name="callback">所有AB资源加载后的回调方法</param>
        public void LoadABCacheAsync(string uriPrefix, string[] abNames, Action callback = null)
        {
            _abLoadHelper.LoadABCacheAsync(uriPrefix, abNames, callback);
        }

        /// <summary>
        /// 异步下载abNames数组中所有的AssetBundle资源
        /// </summary>
        /// <param name="abNames">需要下载的AB名数组</param>
        /// <param name="url">AB包资源的统一下载地址</param>
        /// <param name="saveDirPath">AB资源保存路径</param>
        /// <param name="callback">资源下载后的回调方法,会传入AB包名和是否保存本地成功</param>
        public void DownLoadABAsync(string[] abNames, string url, string saveDirPath,
            Action<string, bool> callback = null)
        {
            _abLoadHelper.DownLoadABAsync(url, abNames, saveDirPath, callback);
        }

        #endregion

        #region 加载AB资源/Asset资源-同步加载

        /// <summary>
        /// 设置AB包配置表
        /// </summary>
        /// <param name="dataFormat">配置表的数据格式</param>
        /// <param name="content">配置表内容</param>
        /// <returns>返回配置表</returns>
        public AssetBundleConfig SetABConfig(DataFormat dataFormat, string content)
        {
            return _abLoadHelper.SetABConfig(dataFormat, content);
        }

        /// <summary>
        /// 加载本地AB配置表
        /// </summary>
        /// <param name="targetPath">配置表的路径</param>
        /// <param name="dataFormat">数据格式类型</param>
        /// <returns>返回配置表类型</returns>
        public AssetBundleConfig LoadABConfig(DataFormat dataFormat, string targetPath)
        {
            return _abLoadHelper.LoadABConfig(dataFormat, targetPath);
        }

        /// <summary>
        /// 同步加载本地资源对象
        /// 如果缓存的资源列表中已经存储资源对象，则直接返回
        /// 如果当前处于编辑器模拟模式，则直接从本地加载对应文件
        /// </summary>
        /// <param name="assetName">资源对象的名字</param>
        /// <typeparam name="T">资源的类型</typeparam>
        /// <returns>返回该资源</returns>
        public T LoadResource<T>(string assetName) where T : Object
        {
            return LoadResource<T>(assetName, "");
        }

        /// <summary>
        /// 同步加载本地资源对象
        /// 如果缓存的资源列表中已经存储资源对象，则直接返回
        /// 如果当前处于编辑器模拟模式，则直接从本地加载对应文件
        /// 如果资源列表中没有存储，则新加载本地的AB包资源
        /// </summary>
        /// <param name="assetName">包含后缀的文件名</param>
        /// <param name="abDirPath">AB包资源在磁盘上所在的文件夹路径</param>
        /// <typeparam name="T">资源的类型</typeparam>
        /// <returns>返回该资源</returns>
        public T LoadResource<T>(string assetName, string abDirPath) where T : Object
        {
            string assetPath = GetAssetPath(assetName);
            if (assetPath.IsNullOrEmpty())
            {
                TLogTool.Error($"未查找到{assetName}对应的资源路径");
                return null;
            }

            uint crc = CRC32.GetCRC32(assetPath);
            ResourceItem item = GetCacheResourceItem(crc);

            if (item != null && item.obj != null)
            {
                TLogTool.Info($"【加载缓存资源对象】从缓存中加载资源对象{assetName}完成\n资源路径：" + assetPath);
                return item.obj as T;
            }

            T obj = null;

#if UNITY_EDITOR
            if (_isEditorMode)
            {
                item = _abLoadHelper.GetResourceItem(assetPath);
                obj = item.obj != null ? item.obj as T : LoadAssetInEditor<T>(assetPath);
            }
#endif

            if (obj == null)
            {
                item = _abLoadHelper.LoadResItem(crc, abDirPath);

                if (item != null && item.assetbundle != null)
                {
                    obj = item.obj != null ? item.obj as T : item.assetbundle.LoadAsset<T>(item.assetName);
                }
            }

            if (obj != null)
                CacheResource(ref item, crc, obj);

            return obj;
        }

        /// <summary>
        /// 释放AB包资源
        /// 如果cache为true，当引用次数为0时，AB包资源将被存储在双向链表中，等待下次调用
        /// 如果cache为false，当引用次数为0时，资源对象会被置空，对应的AB包资源也会被完全卸载
        /// </summary>
        /// <param name="obj">需要释放的资源对象</param>
        /// <param name="cache">是否缓存下来</param>
        /// <returns>返回是否释放成功</returns>
        public bool ReleaseResource(Object obj, bool cache = false)
        {
            if (obj == null || assetDic.Count <= 0) return false;

            //查看资源列表中是否存在该资源
            ResourceItem item = null;
            foreach (ResourceItem res in assetDic.Values)
            {
                if (res.GUID == obj.GetInstanceID())
                {
                    item = res;
                    break;
                }
            }

            if (item == null)
            {
                TLogTool.Error($"【释放资源对象】资源缓存列表里不存在{obj.name}资源，或已被释放");
                return false;
            }

            item.refCount--;
            TLogTool.Info($"【释放资源对象】释放{obj.name}资源对象的引用，还剩{item.refCount}次引用");

            DestroyResourceItem(item, cache);

            return true;
        }

        #endregion

        #region 加载Asset资源-异步加载

        /// <summary>
        /// 初始化用于异步加载资源的协程
        /// </summary>
        public void InitCoroutine(MonoBehaviour mono, string abDirPath)
        {
            _loadingAssetArray = new List<LoadAssetAsyncPara>[3];
            _loadingAssetDic = new Dictionary<uint, LoadAssetAsyncPara>();

            for (int i = 0; i < _loadingAssetArray.Length; i++)
            {
                _loadingAssetArray[i] = new List<LoadAssetAsyncPara>();
            }

            mono.StartCoroutine(LoadLocalResAsync(abDirPath));
        }

        /// <summary>
        /// 异步加载本地资源
        /// </summary>
        /// <param name="path">资源路径</param>
        /// <param name="priority">资源加载的优先级</param>
        /// <param name="callback">资源加载的回调函数</param>
        /// <param name="para1">回调函数的传参</param>
        public void LoadLocalResAsync(Type type, string path, LoadAssetAsyncPriority priority,
            Action<string, Object, object> callback, object para1)
        {
            LoadLocalResAsync(type, path, priority, (str, obj) => { callback?.Invoke(str, obj, para1); });
        }

        /// <summary>
        /// 异步加载本地资源
        /// </summary>
        /// <param name="assetName">资源名称</param>
        /// <param name="priority">资源加载的优先级</param>
        /// <param name="callback">资源加载的回调函数</param>
        public void LoadLocalResAsync(Type type, string assetName, LoadAssetAsyncPriority priority,
            Action<string, Object> callback)
        {
            string assetPath = GetAssetPath(assetName);
            if (assetPath.IsNullOrEmpty())
            {
                TLogTool.Error("异步加载资源的路径不能为空");
                return;
            }

            uint crc = CRC32.GetCRC32(assetPath);

            //如果缓存池内已经存在ResourceItem，则直接使用
            ResourceItem item = GetCacheResourceItem(crc);
            if (item != null)
            {
                callback?.Invoke(assetPath, item.obj);
                return;
            }

            //如果缓存池内不存在ResourceItem，则加入到需要加载的异步资源列表中
            if (!_loadingAssetDic.TryGetValue(crc, out var para) || para == null)
            {
                para = TReferencePool.Acquire<LoadAssetAsyncPara>();
                para.crc = crc;
                para.path = assetPath;
                para.type = type;
                para.priority = priority;
                _loadingAssetDic.Add(crc, para);
                _loadingAssetArray[(int) priority].Add(para);
            }

            para.callback += callback;
        }

        #endregion

        #region Helper Func

        /// <summary>
        /// 获取当前下载的进度
        /// </summary>
        /// <returns>返回进度值</returns>
        public float GetDownloadProgress()
        {
            return _abLoadHelper.GetDownloadProgress();
        }

        /// <summary>
        /// 根据资源名，返回对应的资源路径
        /// </summary>
        /// <param name="assetName">资源名</param>
        /// <returns>资源路径</returns>
        public string GetAssetPath(string assetName)
        {
            return _abLoadHelper.GetAssetPath(assetName);
        }

        /// <summary>
        /// 根据资源名，返回对应的资源路径
        /// </summary>
        /// <param name="assetNames">资源名</param>
        /// <returns>资源路径</returns> 
        public List<string> GetAssetPath(string[] assetNames)
        {
            List<string> paths = new List<string>();
            for (int i = 0; i < assetNames.Length; i++)
            {
                string path = GetAssetPath(assetNames[i]);
                if (path.IsNullOrEmpty() == false)
                    paths.Add(path);
            }

            return paths;
        }

        /// <summary>
        /// 根据AB包名，判断是否已经加载了
        /// </summary>
        /// <param name="abName">需要判断是否加载的AB名</param>
        /// <returns>已经加载了返回true,否则返回false</returns>
        public bool HasLoadedAB(string abName)
        {
            uint crc = CRC32.GetCRC32(abName);
            return AbItemDic.ContainsKey(crc);
        }

        #endregion

        #region Internal Func

        /// <summary>  
        /// 清空缓存
        /// </summary>
        private void Clear()
        {
            List<ResourceItem> resourceItems = new List<ResourceItem>();
            foreach (var item in assetDic.Values)
            {
                resourceItems.Add(item);
            }

            foreach (ResourceItem item in resourceItems)
            {
                item.refCount = 0;
                DestroyResourceItem(item);
            }

            resourceItems.Clear();
            assetDic.Clear();
        }

        /// <summary>
        /// 缓存资源
        /// </summary>
        /// <param name="item">用来缓存AB资源的ResourceItem</param>
        /// <param name="crc">资源路径的Crc值</param>
        /// <param name="obj">需要缓存的资源对象</param>
        /// <param name="addRefCount">增加引用次数</param>
        private void CacheResource(ref ResourceItem item, uint crc, Object obj, int addRefCount = 1)
        {
            if (item == null)
            {
                TLogTool.Error("缓存的ResourceItem不能为空");
                return;
            }

            if (obj == null)
            {
                TLogTool.Error("缓存的资源对象不能为空");
                return;
            }

            item.crc = crc;
            item.obj = obj;
            item.GUID = obj.GetInstanceID();
            item.lastUsedTime = Time.realtimeSinceStartup;
            item.refCount += addRefCount;

            if (assetDic.ContainsKey(item.crc))
                assetDic[item.crc] = item;
            else
                assetDic.Add(item.crc, item);
        }

        //TODO 如果缓存太多，清除最早没有使用的资源
//        protected void WashOut()
//        {
//            while (true)
//            {
//                if (_NoRefAssetMapList.Count() <= 0) break;
//            }
//        }

        //强制销毁无用的资源
        private void DestroyResourceItem(ResourceItem item, bool cache = false)
        {
            if (item == null || item.refCount > 0) return;

            if (cache) //缓存资源，不清除
            {
                _noRefAssetMapList.InsertToFirst(item);
            }
            else
            {
                item.obj = null;
                assetDic.Remove(item.crc);

#if UNITY_EDITOR
                //编辑器模拟模式下，则不需要释放AB包处理
                if (_isEditorMode) return;
#endif

                _abLoadHelper.ReleaseAsset(item);

#if UNITY_EDITOR
                //Resources.UnloadUnusedAssets同样可以卸载由AssetBundle.Load加载的资源，只是前提是其对应的AssetBundle已经调用Unload(false)，且并没有被引用。
                //不能释放AB包中东西，但能释放从AB包中加载出来的资源，也可以释放场景中的资源
                //过多的操作，也可能导致卡顿
                Resources.UnloadUnusedAssets();
#endif
            }
        }

#if UNITY_EDITOR
        private T LoadAssetInEditor<T>(string path) where T : Object
        {
            string assetName = Path.GetFileName(path);
            T t = UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
            if (t != null)
                TLogTool.Info($"【编辑器资源加载模式】加载资源{assetName}完成\n加载路径：" + path);
            else
                TLogTool.Error($"【编辑器资源加载模式】加载资源{assetName}失败\n加载路径：" + path);
            return t;
        }

        private Object LoadAssetInEditor(string path, Type type)
        {
            string assetName = Path.GetFileName(path);
            Object t = UnityEditor.AssetDatabase.LoadAssetAtPath(path, type);
            if (t != null)
                TLogTool.Info($"【编辑器资源加载模式】加载资源{assetName}完成\n加载路径：" + path);
            else
                TLogTool.Error($"【编辑器资源加载模式】加载资源{assetName}失败\n加载路径：" + path);
            return t;
        }
#endif

        //获取缓存里的ResourceItem
        private ResourceItem GetCacheResourceItem(uint crc, int addRefCount = 1)
        {
            if (assetDic.TryGetValue(crc, out var item) && item != null)
            {
                item.refCount += addRefCount;
                item.lastUsedTime = Time.realtimeSinceStartup;
            }

            return item;
        }

        IEnumerator LoadLocalResAsync(string abDirPath)
        {
            long lastYieldTime = DateTime.Now.Ticks;
            while (true)
            {
                bool haveYield = false;
                for (int i = 0; i < 3; i++)
                {
                    List<LoadAssetAsyncPara> loadingList = _loadingAssetArray[i];
                    if (loadingList.Count <= 0) continue;
                    LoadAssetAsyncPara loadingItem = loadingList[0];
                    loadingList.RemoveAt(0);

                    Object obj = null;
                    ResourceItem item = null;
#if UNITY_EDITOR
                    if (_isEditorMode)
                    {
                        //注意：在编辑器模拟模式中，加载的是Assets目录下的资源，而在Assets目录中图片是Texture2D类型
                        obj = LoadAssetInEditor(loadingItem.path, loadingItem.type);
                        yield return new WaitForSeconds(0.1f); //模拟异步加载的时间
                        item = _abLoadHelper.GetResourceItem(loadingItem.path);
                    }
#endif
                    if (obj == null)
                    {
                        item = _abLoadHelper.LoadResItem(loadingItem.crc, abDirPath);
                        if (item != null && item.assetbundle != null)
                        {
                            AssetBundleRequest request =
                                item.assetbundle.LoadAssetAsync(item.assetName, loadingItem.type);
                            yield return request;
                            if (request.isDone)
                            {
                                obj = request.asset;
                                TLogTool.Info($"【异步加载资源对象】从AB包{item.abName}中异步加载资源{obj.name}完成");
                            }

                            lastYieldTime = DateTime.Now.Ticks;
                        }
                    }

                    //缓存加载的资源
                    CacheResource(ref item, loadingItem.crc, obj, loadingItem.callback.GetInvocationList().Length);

                    //加载完后执行回调函数
                    loadingItem.callback.Invoke(loadingItem.path, obj);

                    _loadingAssetDic.Remove(loadingItem.crc);
                    TReferencePool.Release(loadingItem);

                    if (DateTime.Now.Ticks - lastYieldTime > LOADINGTIME)
                    {
                        yield return 0;
                        lastYieldTime = DateTime.Now.Ticks;
                        haveYield = true;
                    }
                }

                if (!haveYield && DateTime.Now.Ticks - lastYieldTime > LOADINGTIME)
                {
                    yield return 0;
                    lastYieldTime = DateTime.Now.Ticks;
                }
            }
        }

        #endregion
    }

    //中间类
    public class LoadAssetAsyncPara : IReference
    {
        public uint crc;
        public string path;
        public Type type;
        public LoadAssetAsyncPriority priority = LoadAssetAsyncPriority.Low;
        public Action<string, Object> callback;

        public void Clear()
        {
            crc = 0;
            path = string.Empty;
            priority = LoadAssetAsyncPriority.Low;
            callback = null;
        }
    }

    public enum LoadAssetAsyncPriority
    {
        High = 0,
        Middle,
        Low
    }

    public enum DataFormat
    {
        Json,
        Encrypt
    }
}